#define FUSE_USE_VERSION 25
#define _FILE_OFFSET_BITS 64

#include "config.h"

#include "fuse-duplex.h"
#include "encryption.h"
#include "hash.h"

#include <string>
#include <iostream>
#include <map>
#include <cstring>
#include <cstdio>

#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <dirent.h>

extern "C" {
   #include "debug.h"
   #include "sha1.h"
   #include "http_curl.h"
   #include "smalloc.h"
   #include "log.h"
}

using namespace std;

map<string, string> files;
uid_t uid = 0;
gid_t gid = 0;
string entitlement = "/etc/cernvm/entitlement";
string entitlement_key = "";
string entitlement_id = "";
string base_url = "";
string mountpoint = "";
string proxies = "";
int master_timeout = 2;


static void to_stat(const string &key, struct stat * const s) {
   memset(s, 0, sizeof(*s));
   s->st_dev = 1;
   s->st_ino = 0;
   s->st_mode = S_IFREG | 0400;
   s->st_nlink = 1;
   s->st_uid = uid;
   s->st_gid = gid;
   s->st_rdev = 1;
   s->st_size = key.length();
   s->st_blksize = 65536;
   s->st_blocks = 1+key.length()/512;
   time_t now = time(NULL);
   s->st_atime = now;
   s->st_mtime = now;
   s->st_ctime = now;
}


static void to_stat_root(struct stat * const s) {
   memset(s, 0, sizeof(*s));
   s->st_dev = 1;
   s->st_ino = 0;
   s->st_mode = S_IFDIR | 0400;
   s->st_nlink = 2;
   s->st_uid = uid;
   s->st_gid = gid;
   s->st_rdev = 1;
   s->st_size = 4096;
   s->st_blksize = 65536;
   s->st_blocks = 1+4096/512;
   time_t now = time(NULL);
   s->st_atime = now;
   s->st_mtime = now;
   s->st_ctime = now;
}


static bool load_entitlement(const string &filename) {
   const unsigned lhead = 4+2*encryption::KEY_LENGTH;
   char buf[lhead]; ///< "CVM-<key>"
   
   FILE *fp = fopen(filename.c_str(), "r");
   if (!fp) {
      logmsg("Failed to open entitlement %s", filename.c_str());
      return false;
   }
   
   if (fread(buf, 1, lhead, fp) != lhead) {
      fclose(fp);
      logmsg("Failed to load entitlement %s", filename.c_str());
      return false;
   }
   rewind(fp);
   hash::t_sha1 sha1;
   sha1_file_fp(fp, sha1.digest);
   fclose(fp);
   
   /* Cut "CVM-" */
   entitlement_key = string(buf+4, 2*encryption::KEY_LENGTH);
   entitlement_id = sha1.to_string();
   
   return encryption::load_key(entitlement_key);
}


static bool load_cache(const string &dir_name) {
   DIR *dir = opendir(dir_name.c_str());
   if (!dir) return false;
   
   struct dirent64 *d;
   while ((d = readdir64(dir)) != NULL) {
      const string name = string(d->d_name);
      const string path = dir_name + "/" + name;
      struct stat64 info;

      if (lstat64(path.c_str(), &info) != 0)
         continue;         
      if (!S_ISREG(info.st_mode))
         continue;
      
      FILE *fp = NULL;
      size_t lbuf = 2*encryption::KEY_LENGTH;
      char buf[lbuf];
      if (((fp = fopen(path.c_str(), "r")) == NULL) ||
          (fread(buf, 1, lbuf, fp) != lbuf))
      {
         if (fp) fclose(fp);
         cerr << "Warning: failed to read " << path << endl;
         continue;
      }
      fclose(fp);

      hash::t_sha1 sha1;
      sha1_mem(buf, lbuf, sha1.digest);
      
      if (sha1.to_string() != name) {
         cerr << "Warning: " << path << "is not a valid key file" << endl;
         continue;
      }
      
      files.insert(make_pair(name, string(buf, lbuf)));
   }
   
   closedir(dir);
   return true;
}


static void write_cache(const string &dir_name) {
   for (map<string, string>::const_iterator i = files.begin(), iEnd = files.end();
        i != iEnd; ++i)
   {
      const string path = dir_name + "/" + i->first;
      
      struct stat64 info;
      if (stat64(path.c_str(), &info) != 0) {
         FILE *fp = NULL;
         if (((fp = fopen(path.c_str(), "w")) == NULL) ||
             (fwrite(&((i->second)[0]), 1, i->second.length(), fp)) != i->second.length())
         {
            logmsg("Failed to write key %s", i->first.c_str());
         }
         if (fp) fclose(fp);
      }
   }
}
   
   
static int cvmfs_keys_getattr(const char *path, struct stat *info) {
   // check local map, download on demand (reload entitlement if needed)
   if (string(path) == "/") {
      to_stat_root(info);
      return 0;
   }
   
   const string id(path+1); ///< cut leading "/"
   /* Sanity check */
   if (id.length() != 40)
      return -ENOENT;
   for (int i = 0; i < 40; ++i) {
      if ((id[i] < '0') || (id[i] > 'f') ||
          ((id[i] > '9') && (id[i] < 'a')))
      {
         return -ENOENT;
      }
   }
   
   map<string, string>::const_iterator i = files.find(id);
   if (i != files.end()) {
      to_stat(i->second, info);
      return 0;
   }
   
   struct mem_url mem_url;
   int result = curl_download_mem((base_url + "/" + id + "/" + entitlement_id).c_str(),
                                  &mem_url, 0, 0);
   if (result != CURLE_OK)
      return -ENOENT;

   if (mem_url.size <= 2*encryption::IV_LENGTH)
      return -EIO;
      
   const string iv_str = string(mem_url.data, 2*encryption::IV_LENGTH);
   unsigned char iv[encryption::IV_LENGTH];
   encryption::hex2bin(iv_str, iv, encryption::IV_LENGTH);
   
   unsigned char *key;
   unsigned klen;
   if (!encryption::decrypt_mem(iv, (const unsigned char *)(mem_url.data+2*encryption::IV_LENGTH), 
                                 mem_url.size-2*encryption::IV_LENGTH, &key, &klen))
   {
      return -EIO;
   }
   
   hash::t_sha1 sha1;
   sha1_mem(key, klen, sha1.digest);
   if (sha1.to_string() != id) {
      free(key);
      return -EIO;
   }
   
   const string str_key((char *)key, klen);
   free(key);
   to_stat(str_key, info);
   files.insert(make_pair(id, str_key));
   
   return 0;   
}


static int cvmfs_keys_readlink(const char *path __attribute__((unused)), 
                               char *buf, size_t size __attribute__((unused))) 
{
   return -ENOENT;
}


static int cvmfs_keys_readdir(const char *path, 
                              void *buf, 
                              fuse_fill_dir_t filler, 
                              off_t offset __attribute__((unused)),
                              struct fuse_file_info *fi __attribute__((unused)))
{
   if (string(path) != "/")
      return -ENOENT;

   struct stat dir_info;
   to_stat_root(&dir_info);
   filler(buf, ".", &dir_info, 0);
   
   struct stat parent_info;
   if (stat((string(path) + "..").c_str(), &parent_info) == 0)
      filler(buf, "..", &parent_info, 0);
      
   for (map<string, string>::const_iterator i = files.begin(), iEnd = files.end();
        i != iEnd; ++i)
   {
      struct stat info;
      to_stat(i->second, &info);
      filler(buf, i->first.c_str(), &info, 0);
   }
   
   return 0;
}
   

static int cvmfs_keys_open(const char *path, struct fuse_file_info *fi)
{
   const string id(path+1); ///< cut leading "/"
   
   map<string, string>::const_iterator i = files.find(id);
   if (i == files.end())
      return -ENOENT;
   
   return 0;
}


static int cvmfs_keys_read(const char *path, 
                           char *buf, 
                           size_t size, 
                           off_t offset, 
                           struct fuse_file_info *fi)
{
   const string id(path+1); ///< cut leading "/"
   
   map<string, string>::const_iterator i = files.find(id);
   if (i == files.end())
      return -ENOENT;
   
   const string key = i->second;

   if ((offset < 0) || ((unsigned)offset >= key.length()))
      return -EINVAL;
   
   size_t nbytes = key.length()-offset > size ? size : key.length()-offset;
   memcpy(buf, &(key[offset]), nbytes);
   
   return nbytes;
}


static int cvmfs_keys_release(const char *path __attribute__((unused)),
                              struct fuse_file_info *fi __attribute__((unused)))
{
   return 0;
}


/**
 * Not implemented.
 */
#if FUSE_USE_VERSION==22
static int cvmfs_keys_statfs(const char *path __attribute__((unused)), struct statfs *info)
#else
static int cvmfs_keys_statfs(const char *path __attribute__((unused)), struct statvfs *info)
#endif
{
   /* We will return 0 which will cause the fs 
      to be ignored in "df" for example */
   memset(info, 0, sizeof(*info));
   return 0;
}
   

/**
 * \return -EROFS
 */

static int cvmfs_keys_chmod(const char *path, mode_t mode) {
   return -EROFS;
}
   
   
static int cvmfs_keys_mkdir(const char *path __attribute__((unused)), 
                            mode_t mode __attribute__((unused)))
{
   return -EROFS;
}
   
   
static int cvmfs_keys_unlink(const char *path __attribute__((unused))) {
   return -EROFS;
}
   
   
static int cvmfs_keys_rmdir(const char *path __attribute__((unused))) {
   return -EROFS;
}
   
   
static int cvmfs_keys_symlink(const char *from __attribute__((unused)), 
                              const char *to __attribute__((unused)))
{
   return -EROFS;
}
   
   
static int cvmfs_keys_rename(const char *from __attribute__((unused)), 
                             const char *to __attribute__((unused)))
{
   return -EROFS;
}


static int cvmfs_keys_link(const char *from __attribute__((unused)), 
                           const char *to __attribute__((unused)))
{
   return -EROFS;
}


static int cvmfs_keys_chown(const char *path __attribute__((unused)), 
                            uid_t uid __attribute__((unused)), 
                            gid_t gid __attribute__((unused)))
{
   return -EROFS;
}


static int cvmfs_keys_truncate(const char *path __attribute__((unused)), 
                               off_t size __attribute__((unused)))
{
   return -EROFS;
}


static int cvmfs_keys_utime(const char *path __attribute__((unused)), 
                            struct utimbuf *buf __attribute__((unused)))
{
   return -EROFS;
}


static int cvmfs_keys_write(const char *path __attribute__((unused)),
                            const char *buf __attribute__((unused)), 
                            size_t size __attribute__((unused)), 
                            off_t offset __attribute__((unused)), 
                            struct fuse_file_info *fi __attribute__((unused)))
{
   return -EROFS;
}


static int cvmfs_keys_mknod(const char *path __attribute__((unused)), 
                            mode_t mode __attribute__((unused)), 
                            dev_t rdev __attribute__((unused)))
{
   return -EROFS;
}


/** 
 * Puts the callback functions in one single structure
 */
static void set_cvmfs_keys_ops(struct fuse_operations *cvmfs_keys_operations) {
   /* Implemented */
   cvmfs_keys_operations->getattr  = cvmfs_keys_getattr;
   cvmfs_keys_operations->readlink = cvmfs_keys_readlink;
   cvmfs_keys_operations->readdir  = cvmfs_keys_readdir;
   cvmfs_keys_operations->open	  = cvmfs_keys_open;
   cvmfs_keys_operations->read	  = cvmfs_keys_read;
   cvmfs_keys_operations->release  = cvmfs_keys_release;
   
   /* Return EROFS (read-only filesystem) */
   cvmfs_keys_operations->chmod    = cvmfs_keys_chmod;
   cvmfs_keys_operations->mkdir    = cvmfs_keys_mkdir;
   cvmfs_keys_operations->unlink   = cvmfs_keys_unlink;
   cvmfs_keys_operations->rmdir    = cvmfs_keys_rmdir;
   cvmfs_keys_operations->symlink  = cvmfs_keys_symlink;
   cvmfs_keys_operations->rename   = cvmfs_keys_rename;
   cvmfs_keys_operations->chown    = cvmfs_keys_chown;
   cvmfs_keys_operations->link	  = cvmfs_keys_link;
   cvmfs_keys_operations->truncate = cvmfs_keys_truncate;
   cvmfs_keys_operations->utime    = cvmfs_keys_utime;
   cvmfs_keys_operations->write    = cvmfs_keys_write;
   cvmfs_keys_operations->mknod    = cvmfs_keys_mknod;
   cvmfs_keys_operations->statfs   = cvmfs_keys_statfs;
}


/**
 * One single structure to contain the file system options.
 * Strings(char *) must be deallocated by the user 
 */ 
struct cvmfs_keys_opts {
   unsigned timeout;
   char     *hostname;
   char     *entitlement;
   char     *proxies;
   int      entry_point;
   int      grab_mountpoint;
};

/* Follow the fuse convention for option parsing */
enum {
   KEY_HELP,
   KEY_VERSION,
   KEY_FOREGROUND
};
#define CVMFS_KEYS_OPT(t, p, v) { t, offsetof(struct cvmfs_keys_opts, p), v }
#define CVMFS_KEYS_SWITCH(t, p) { t, offsetof(struct cvmfs_keys_opts, p), 1 }
static struct fuse_opt cvmfs_keys_array_opts[] = {
   CVMFS_KEYS_OPT("timeout=%u",          timeout, master_timeout),
   CVMFS_KEYS_OPT("entitlement=%s",      entitlement, 0),
   CVMFS_KEYS_OPT("proxies=%s",          proxies, 0),
   CVMFS_KEYS_OPT("entry_point=%d",      entry_point, 1),
   CVMFS_KEYS_SWITCH("grab_mountpoint",  grab_mountpoint),
   
   FUSE_OPT_KEY("-V",            KEY_VERSION),
   FUSE_OPT_KEY("--version",     KEY_VERSION),
   FUSE_OPT_KEY("-h",            KEY_HELP),
   FUSE_OPT_KEY("--help",        KEY_HELP),
   FUSE_OPT_KEY("-f",            KEY_FOREGROUND),
   FUSE_OPT_KEY("-d",            KEY_FOREGROUND),
   FUSE_OPT_KEY("debug",         KEY_FOREGROUND),
   {0, 0, 0},
};


struct cvmfs_keys_opts cvmfs_keys_opts;
struct fuse_args fuse_args;


/** 
 * Display the usage message.
 * It will be done when we requested (the flag "-h" for example),
 * but also when an unidentified option is found.
 */
static void usage(const char *progname) {
   fprintf(stderr,
   "CernVM Key Ring FS\n"
   "Copyright (c) 2010- CERN\n"
   "All rights reserved\n\n"
   "Please visit http://cernvm.cern.ch/project/info for license details and author list.\n\n");

   if (progname)
      fprintf(stderr,
      "usage: %s [options] <url>[,<url>]* <mountpath>\n\n", progname);

   fprintf(stderr,
      "where options are:\n"
      " -o opt,[opt...]  mount options\n\n"
      "CernVM Key Ring FS options: \n"
      " -o timeout=SECONDS         Timeout for network operations (default is %d)\n"
      " -o entitlement=FILE        CernVM entitlement file\n"
      " -o proxies=HTTP_PROXIES    Set the HTTP proxy list, such as 'proxy1|proxy2;DIRECT'\n"
      " -o entry_point=NUMBER      For multiple urls, specify which one to start with for data (starting with 0)\n"
      "                            (default is 1, catalogs will always be fetched from url 0)\n"
      " -o grab_mountpoint         Give ownership of the mountpoint to the user before mounting (automount hack)\n"
      " -d                         Debug mode, stay foreground\n",
      master_timeout
   );
}


/**
 * Since certain fileds in cvmfs_opts are filled automatically when parsing it, \
 * we needed a procedure to free the space.
 */
static void free_cvmfs_keys_opts(struct cvmfs_keys_opts *opts) {
   if (opts->hostname)       free(opts->hostname);
   if (opts->proxies)        free(opts->proxies);
   if (opts->entitlement)    free(opts->entitlement);
}


/**
 * Checks whether the given option is one of our own options
 * (if it's not, it probably belongs to fuse).
 */
static int is_cvmfs_keys_opt(const char *arg) {
   if (arg[0] != '-') {
      unsigned arglen = strlen(arg);
      const char **o;
      for (o = (const char**)cvmfs_keys_array_opts; *o; o++) {
         unsigned olen = strlen(*o);
         if ((arglen > olen && arg[olen] == '=') && (strncasecmp(arg, *o, olen) == 0))
            return 1;
      }
   }
   return 0;
}


/**
 * The callback used when fuse is parsing all the options
 * We separate CVMFS options from FUSE options here.
 *
 * \return On success zero, else non-zero
 */
static int cvmfs_keys_opt_proc(void *data __attribute__((unused)), const char *arg, int key,
                               struct fuse_args *outargs)
{   
   switch (key) {
      case FUSE_OPT_KEY_OPT:
         if (is_cvmfs_keys_opt(arg)) {
            /* If this is a "-o" option and is not one of ours, we assume that this must
               be used when mounting fuse not when instanciating the file system...            
               It can't be one of our option if it doesnt match the template */
            return 0;
         }
         if (strstr(arg, "uid=")) {
            uid = atoi(arg+4);
            return 1;
         }
         else if (strstr(arg, "gid=")) {
            gid = atoi(arg+4);
            return 1;
         }
         else if (strstr(arg, "nonempty") || strstr(arg, "allow_other")) {
            return 1;
         }
         cerr << "Unknown option" << endl;
         return -1;
         
      case FUSE_OPT_KEY_NONOPT:
         if (!cvmfs_keys_opts.hostname && strstr(arg, "http://")) {
            /* If we receive a parameter that contains "http://" 
               we know for sure that it's our remote server */
            cvmfs_keys_opts.hostname = strdup(arg);
         } else {
            /* If we receive any other string, we take it as the mount point. */
            fuse_opt_add_arg(outargs, arg);
            mountpoint = arg;
         }
         return 0;
         
      case KEY_HELP:
         usage(outargs->argv[0]);
         exit(0);
         
      case KEY_VERSION:
         fprintf(stderr, "CernVM Key Ring FS version %s\n", PACKAGE_VERSION);
         exit(0);
         
      case KEY_FOREGROUND:
         fuse_opt_add_arg(outargs, "-f");
         return 0;
         
      default:
         fprintf(stderr, "internal error\n");
         abort();
   }
}

int main(int argc, char *argv[]) {
   int result = 1;
   bool options_ready = false;
   bool curl_ready = false;
   bool encryption_ready = false;
   int num_hosts;
   
   umask(007);
   
   struct fuse_args fuse_args;
   fuse_args.argc = argc;
   fuse_args.argv = (char **)smalloc(argc * sizeof(char *));
   for (int i = 0; i < argc; ++i)
      fuse_args.argv[i] = strdup(argv[i]);
   fuse_args.allocated = 1;
   fuse_opt_add_arg(&fuse_args, "-ononempty,allow_other");
   cvmfs_keys_opts.entry_point = 1;
   if ((fuse_opt_parse(&fuse_args, &cvmfs_keys_opts, cvmfs_keys_array_opts, cvmfs_keys_opt_proc) != 0) || 
       !cvmfs_keys_opts.hostname) 
   {
      usage(argv[0]);
      goto cvmfs_keys_cleanup;
   }

   /* Fill cvmfs option variables from Fuse options */
   if (!uid) uid = getuid();
   if (!gid) gid = getgid();
   if (cvmfs_keys_opts.entitlement) entitlement = cvmfs_keys_opts.entitlement;
   if (cvmfs_keys_opts.proxies) proxies = cvmfs_keys_opts.proxies;
   if (cvmfs_keys_opts.timeout != 0) master_timeout = cvmfs_keys_opts.timeout;
   /* seperate first host from hostlist */
   unsigned iter_hostname;
   for (iter_hostname = 0; iter_hostname < strlen(cvmfs_keys_opts.hostname); ++iter_hostname) {
      if (cvmfs_keys_opts.hostname[iter_hostname] == ',') break;
   }
   if (iter_hostname == 0) base_url = "";
   else base_url = string(cvmfs_keys_opts.hostname, iter_hostname);
   options_ready = true;
   
   /* Grab mountpoint */
   if (cvmfs_keys_opts.grab_mountpoint) {
      if ((chown(mountpoint.c_str(), uid, gid) != 0) ||
          (chmod(mountpoint.c_str(), 0755) != 0))
      {
         cerr << "Failed to grab mountpoint (" << errno << ")" << endl;
         goto cvmfs_keys_cleanup;
      }
   }
   
   /* Drop rights */
   if ((uid != 0) || (gid != 0)) {
      cout << "CernVM Key Ring FS: running with credentials " << uid << ":" << gid << endl;
      if ((setgid(gid) != 0) || (setuid(uid) != 0)) {
         cerr << "Failed to drop credentials" << endl;
         goto cvmfs_keys_cleanup;
      }
   }
   
   /* Check mountpoint */
   if (utimes(mountpoint.c_str(), NULL) != 0) {
      cerr << "Failed to write in " << mountpoint << endl;
      cerr << "Is the CernVM Key Ring FS already mounted here?" << endl;
      goto cvmfs_keys_cleanup;
   }
   
   /* Set proxy environment, chain of proxies */
   num_hosts = curl_set_host_chain(cvmfs_keys_opts.hostname);
   curl_set_proxy_chain(proxies.c_str());
   curl_set_timeout(master_timeout, master_timeout);
      
   /* Switch to entry point */
   if (cvmfs_keys_opts.entry_point < 0) {
      cerr << "Failure: entry_point must be a positive number" << endl;
      goto cvmfs_keys_cleanup;
   }
   if (num_hosts > 1) {
      for (int i = 1; i <= cvmfs_keys_opts.entry_point%num_hosts; ++i) {
         curl_switch_host();
      }
   }
   curl_ready = true;
   
   encryption::init();
   encryption_ready = true;
   
   if (!load_entitlement(entitlement)) {
      cerr << "Failed to load entitlement" << endl;
      goto cvmfs_keys_cleanup;
   }
   cout << "CernVM Key Ring FS: Using entitlement id " << entitlement_id << endl;
   
   if (!load_cache(mountpoint)) {
      cerr << "Failed to load key cache" << endl;
      goto cvmfs_keys_cleanup;
   }
   
   /* Single threaded */
   fuse_opt_add_arg(&fuse_args, "-s");
   cout << "CernVM Key Ring FS: mounted on " << mountpoint << endl;
   cout << "CernVM Key Ring FS: linking to key server " << base_url << endl;
   static struct fuse_operations cvmfs_keys_ops;
   set_cvmfs_keys_ops(&cvmfs_keys_ops);
   result = fuse_main(fuse_args.argc, fuse_args.argv, &cvmfs_keys_ops);
   
cvmfs_keys_cleanup:
   if (encryption_ready) encryption::fini();
   if (options_ready) {
      fuse_opt_free_args(&fuse_args);
      free_cvmfs_keys_opts(&cvmfs_keys_opts);
   }
   if (result == 0) write_cache(mountpoint);
   
   return result;
}
